﻿ 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Security;

namespace LC.Utils.WCF
{
    public enum WCFOperationResultEnum
    {
        Success,
        ServerNotAvailable
    }


    public delegate void UseServiceDelegateVoid<T>(T proxy);

    /// <summary>
    /// http://stackoverflow.com/questions/573872/what-is-the-best-workaround-for-the-wcf-client-using-block-issue
    /// http://old.iserviceoriented.com/blog/post/Indisposable+-+WCF+Gotcha+1.aspx
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class ServiceWrapper<T>  
    {
        public    string ChannelFactoryConfigName {get;set;}
        public   ChannelFactory<T> _channelFactory = new ChannelFactory<T>();


        IClientChannel proxyInstance = null;
        IContextChannel requestChannel = null;

        ICommunicationObject clientCallbackCommunicationObject = null;
        EventHandler faultedHandlerCallback = null;// (o, s) => Console.WriteLine("Client Channel Faulted.");
        EventHandler closedHandlerCallback = null;// (o, s) => Console.WriteLine("Client Channel Closed.");

        bool isCallback = false;

        public ServiceWrapper()
        {
            this.isCallback = false;
            ConstructWCFProxy();
        }

        /// <summary>
        /// Use this constructor when operating on the callback of a duplex connection
        /// </summary>
        /// <param name="callback"></param>
        public ServiceWrapper(bool isCallback)
        {
            this.isCallback = isCallback;
            ConstructWCFProxy();
        }
        /// <summary>
        /// Use this constructor when you create your bindings in code
        /// </summary>
        /// <param name="channelFactory"></param>
        public ServiceWrapper(ChannelFactory<T> channelFactory)
        {
            this.isCallback = false;
            _channelFactory = channelFactory;                
            ConstructWCFProxy();
        }

        private void ConstructWCFProxy()
        {
            WarnIfUsingWSHTTP();

            if (isCallback)
            {
                ICommunicationObject callback = (ICommunicationObject)OperationContext.Current.GetCallbackChannel<T>();
                GC.KeepAlive(callback);

                //todo: Understand why OperationContext can be null(next step) and callback not null.
                if (callback != null)
                {
                    proxyInstance = (IClientChannel)callback;
                }

                if (OperationContext.Current == null)
                {
                    //TODO DEBUG: Client is no longer connected?  This occured while debugging and the client was restarted
                    System.Diagnostics.Debug.WriteLine("DEBUG: Constructor() was called while OperationContext.Current == null.  Ignoring method call. ");
                    return;
                }
                else
                {
                    //Request Channel
                    requestChannel = OperationContext.Current.Channel;
                     this.CallbackURL = OperationContext.Current.Channel.RemoteAddress;
                }

            }
            else
            {
                if (String.IsNullOrEmpty(ChannelFactoryConfigName))
                    proxyInstance = (IClientChannel)_channelFactory.CreateChannel();
                else
                {
                    _channelFactory = new ChannelFactory<T>(ChannelFactoryConfigName);
                    proxyInstance = (IClientChannel)_channelFactory.CreateChannel();
                }
                // todo: setup event handlers properly.  They leak easily, so do it right.
               // clientCallbackCommunicationObject = ((ICommunicationObject)callback);
               // clientCallbackCommunicationObject..Faulted += faultedHandlerCallback;
                //clientCallbackCommunicationObject.Closed += closedHandlerCallback;

            }



            switch (((IClientChannel)proxyInstance).State )
            {
                case CommunicationState.Closing:
                case CommunicationState.Created:
                case CommunicationState.Closed:
                    {
                        // If you don’t call the “Open” method first, the proxy would be opened internally when the first call is made on the proxy. This is called auto-open
                        // This avoids an implied performance hit
                        // Once you get to this point, you will be able to share the same proxy object among multiple threads
                        // reference: http://blogs.msdn.com/b/wenlong/archive/2007/10/26/best-practice-always-open-wcf-client-proxy-explicitly-when-it-is-shared.aspx

                        try
                        {
                            ((IClientChannel)proxyInstance).Open();

                        }
                        catch (TimeoutException cc)
                        {
                            throw;
                        }
                        catch (CommunicationException cc)
                        {
                            throw;
                        }
                        catch (Exception ee)
                        {

                            throw;
                        }


                        break;
                    }
                case CommunicationState.Faulted:
                    break;
                case CommunicationState.Opened:
                    break;
                case CommunicationState.Opening:
                    break;
                default:
                    break;
            }
          
        }

        private void WarnIfUsingWSHTTP()
        {
            // todo: detect if binding is wshttp.  It is not worth the effort.  Use TCP instead.
            //http://www.dotnetconsult.co.uk/weblog2/PermaLink,guid,b891610a-6b78-4b54-b9a6-4ec81c82b7c0.aspx
            //http://blogs.oracle.com/arungupta/entry/wshttpdualbinding_a_non_interoperable_binding
        }

        /// <summary>
        /// A "smarter" WCF Proxy that handles transient network errors, authentication expiration, and others.
        /// Some information on the faster performance of Reuse
        /// http://blogs.msdn.com/b/wenlong/archive/2007/10/27/performance-improvement-of-wcf-client-proxy-creation-and-best-practices.aspx
        /// 
        /// Session ending...
        /// http://stackoverflow.com/questions/2788142/wcf-how-to-find-out-when-a-session-is-ending
        /// 
        /// LowLevel WCF errors
        /// http://stackoverflow.com/questions/1271887/how-to-handle-low-level-wcf-errors
        /// </summary>
        /// <param name="codeBlock"></param>
        public WCFOperationResultEnum Reuse(UseServiceDelegateVoid<T> codeBlock)
        {
            if (proxyInstance.State == CommunicationState.Faulted)
            {
                System.Diagnostics.Debug.WriteLine("WARNING: WCF Client is automatically recovering from a faulted state.");
                proxyInstance.Abort();

                //todo: not sure if this will reconstruct a faulted WCF client proxy
                try
                {
                    ConstructWCFProxy();
                }
                catch (Exception)
                {
                    switch (proxyInstance.State)
                    {
                        case CommunicationState.Closed:
                        case CommunicationState.Closing:
                        case CommunicationState.Created:
                        case CommunicationState.Faulted:
                            {
                                return WCFOperationResultEnum.ServerNotAvailable;
                            }
                        case CommunicationState.Opened:
                        case CommunicationState.Opening:
                            break;
                        default:
                            break;
                    }
                }
            }

            try
            {
               // requestChannel.Faulted += faultedHandlerRequest;
               // requestChannel.Closed += closedHandlerRequest;
                try
                {
                    codeBlock((T)proxyInstance);

                    // Avoid: The session was closed before message transfer was complete.
                    ///proxyInstance.Close();

                    return WCFOperationResultEnum.Success;
                }
                    // Sometimes this should be retried.  sometimes not.
                catch (CommunicationObjectAbortedException ex)
                {
                    // Object should be discarded if this is reached.  
                    // Debugging discovered "Connection can not be established because it has been aborted" exception message
                    Console.WriteLine("Client Aborted the connection");
                    return WCFOperationResultEnum.ServerNotAvailable;


                    //todo: server fault may have occured, and it resets session:  Retry the following
                    //The underlying connection was closed: A connection that was expected to be kept alive was closed by the server.
                }
                catch (CommunicationObjectFaultedException ex2)
                {
                    Console.WriteLine("Client Died.");
                }
                catch (MessageSecurityException)
                {
                    //http://stackoverflow.com/questions/479618/renewing-a-wcf-client-when-sct-has-expired
                    if (proxyInstance.State != CommunicationState.Faulted)
                    {
                        throw;
                    }

                    proxyInstance.Abort();
                    proxyInstance = (IClientChannel)_channelFactory.CreateChannel();

                    codeBlock((T)proxyInstance);
                }
                catch (TimeoutException TimeOut)
                {
                    /*
                        at Microsoft.Samples.ReliableSecureProfile.InputQueue`1.EndDequeue(IAsyncResult result) in C:\Agent7.Net4\LC.Agent7.AzureHost\Microsoft.Samples.ReliableSecureProfile.MakeConnectionChannel\InputQueue.cs:line 292
   at Microsoft.Samples.ReliableSecureProfile.MakeConnectionDispatcher.EnqueueAndSendAsyncResult.OnContextDequeueCompletedCore(IAsyncResult result) in C:\Agent7.Net4\LC.Agent7.AzureHost\Microsoft.Samples.ReliableSecureProfile.MakeConnectionChannel\MakeConnectionDispatcher.cs:line 251
   at Microsoft.Samples.ReliableSecureProfile.MakeConnectionDispatcher.EnqueueAndSendAsyncResult.OnContextDequeueCompleted(IAsyncResult result) in C:\Agent7.Net4\LC.Agent7.AzureHost\Microsoft.Samples.ReliableSecureProfile.MakeConnectionChannel\MakeConnectionDispatcher.cs:line 235


The message could not be transferred within the allotted timeout of 00:01:00. There was no space available in the reliable channel's transfer window. The time allotted to this operation may have been a portion of a longer timeout.
                     */
                    System.Diagnostics.Debug.WriteLine(TimeOut.Message);

                    if (isCallback == true)
                    {
                        return WCFOperationResultEnum.ServerNotAvailable;
                    }
                }
            }
            finally
            {
                // Avoid the most common type of .NET memory leak
                // Always remove event handlers

                //requestChannel.Faulted -= faultedHandlerRequest;
               // requestChannel.Closed -= closedHandlerRequest;
               
                if (clientCallbackCommunicationObject != null)
                {
                    clientCallbackCommunicationObject.Faulted -= faultedHandlerCallback;
                    clientCallbackCommunicationObject.Faulted -= closedHandlerCallback;
                }

            }

            return WCFOperationResultEnum.Success;
        }


        WeakReference wr = null;
        /// <summary>
        /// Ensures async event handlers are cleaned up
        /// Source
        /// http://stackoverflow.com/questions/271198/will-this-wcf-client-side-code-cause-a-memory-leak
        /// </summary>
        public void ReuseAsync()
        {
            //var proxy = new Proxy();
            //proxy.Complete += (sender, e) =>
            //{
            //    proxy.Close();
            //    wr = new WeakReference(proxy);
            //    proxy = null;
            //};
            //GC.Collect(GC.GetGeneration(wr));
            //GC.WaitForPendingFinalizers();

            //Console.WriteLine("[LAMBDA] Is WeakReference alive? " + wr.IsAlive);
        }

        // USAGE SAMPLE
        //int newOrderId = 0; // need a value for definite assignment
        //Service<IOrderService>.Use(null,orderService=>
        //{
        //  newOrderId = orderService.PlaceOrder(request);
        //}

         
       /// <summary>
       /// A one time use & immediate dispose method for any WCF call.  If you're calling the same WCF service frequently, it's probably better to create an instance class and use the Reuse() method.
       /// </summary>
       /// <param name="ConfigName">Can be null.  Choose a WCF configuration from within your app.config file.</param>
       /// <param name="codeBlock"></param>
        public static void Use(string ConfigName, UseServiceDelegateVoid<T> codeBlock)
        {
            ChannelFactory<T> channelFactory = null;
            if (ConfigName == null)
                channelFactory = new ChannelFactory<T>(); 
            else
                channelFactory = new ChannelFactory<T>(ConfigName);

            Use(channelFactory, codeBlock);
        }
        /// <summary>
        /// Abort the Channel If Necessary
        /// Handle Exceptions When Closing the Channel
        /// Catch Fault Exceptions in the Correct Order
        /// 
        /// A one time use & immediate dispose method for any WCF call.  If you're calling the same WCF service frequently, it's probably better to create an instance class and use the Reuse() method.        
        /// </summary>
        /// <param name="channelFactory"></param>
        /// <param name="codeBlock"></param>
        public static void Use(ChannelFactory<T> channelFactory, UseServiceDelegateVoid<T> codeBlock)
        {        
            IClientChannel proxy = (IClientChannel)channelFactory.CreateChannel();

            bool success = false;
            try
            {
                codeBlock((T)proxy);
                proxy.Close();
                success = true;
            }
            catch (CommunicationObjectAbortedException e)
            {
                throw e;
            }
            catch (CommunicationObjectFaultedException e)
            {
                throw e;
            }
            catch (MessageSecurityException e)
            {
                throw e;
            }
            catch (ChannelTerminatedException)
            {
                proxy.Abort(); // Possibly retry?
            }
            catch (ServerTooBusyException)
            {
                proxy.Abort(); // Possibly retry?
            }
            catch (EndpointNotFoundException)
            {
                proxy.Abort(); // Possibly retry?
            }
            
            // FaultException<T> should always appear above CommunicationException or it will get swallowed
            catch (FaultException<object> rethrow)
            {
                throw rethrow;
            }
            // FaultException should always appear above CommunicationException or it will get swallowed
            catch (FaultException rethrow)
            {
                throw rethrow;               
            }
            catch (CommunicationException)
            {
                proxy.Abort();
            }
            catch (TimeoutException)
            {
                proxy.Abort();
            }
            finally
            {
                if (!success)
                {
                    proxy.Abort();
                }
            }
        }

        public EndpointAddress CallbackURL { get; set; }


        public void Dispose()
        {
            DisposeCommon();
        }

        /// <summary>
        /// This code should be used in a FINALLY block to clean up resources
        /// 
        /// Removes EventHandlers, closes channel, releases resources
        /// 
        ///  Best Practice: Handle Exceptions When Closing the Channel
        ///  
        /// This is called
        /// 1) When the WCF client object is recycled.
        /// 2) When the client application calls System.ServiceModel.ClientBase.Close.
        /// 3) When the client application calls System.ServiceModel.ICommunicationObject.Close.
        /// 4) When the client application calls an operation that is a terminating operation for a session.
        /// </summary>
        public void DisposeCommon()
        {
            // todo: this section needs a lot of work.


            // The following code is from: http://www.vasylevskyi.com/2010/11/correct-wcf-client-proxy-closing.html
            try
            {
                if (proxyInstance.State != CommunicationState.Closed && proxyInstance.State != CommunicationState.Faulted)
                {
                    proxyInstance.Close(); // may throw exception while closing
                }
                else
                {
                    proxyInstance.Abort();
                }
            }
            catch (CommunicationException)
            {
                proxyInstance.Abort();
                throw;
            }
 

            try
            {
                requestChannel.Close();
            }
            catch (Exception)
            {
                throw;
            }

            if (clientCallbackCommunicationObject != null)
                clientCallbackCommunicationObject.Close();

            // TODO: Create a more efficient way of managing event handler disposal
            if (proxyInstance != null)
                EventHelper.RemoveAllEventHandlers(proxyInstance);
            if (requestChannel != null)
                EventHelper.RemoveAllEventHandlers(requestChannel);
            if (clientCallbackCommunicationObject != null)
                EventHelper.RemoveAllEventHandlers(clientCallbackCommunicationObject);
        }
    }

}
